/*
 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.java.swing.plaf.gtk;

import java.awt.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.*;
import javax.swing.*;
import javax.swing.plaf.*;
import javax.swing.plaf.synth.*;

import sun.awt.AppContext;
import sun.awt.UNIXToolkit;
import sun.swing.SwingUtilities2;
import sun.swing.plaf.synth.SynthIcon;

import com.sun.java.swing.plaf.gtk.GTKEngine.WidgetType;

/**
 * @author Scott Violet
 */
class GTKStyle extends SynthStyle implements GTKConstants {

  private static native int nativeGetXThickness(int widgetType);

  private static native int nativeGetYThickness(int widgetType);

  private static native int nativeGetColorForState(int widgetType,
      int state, int typeID);

  private static native Object nativeGetClassValue(int widgetType,
      String key);

  private static native String nativeGetPangoFontName(int widgetType);

  private static final String ICON_PROPERTY_PREFIX = "gtk.icon.";

  static final Color BLACK_COLOR = new ColorUIResource(Color.BLACK);
  static final Color WHITE_COLOR = new ColorUIResource(Color.WHITE);

  static final Font DEFAULT_FONT = new FontUIResource("sansserif",
      Font.PLAIN, 10);
  static final Insets BUTTON_DEFAULT_BORDER_INSETS = new Insets(1, 1, 1, 1);

  private static final GTKGraphicsUtils GTK_GRAPHICS = new GTKGraphicsUtils();

  /**
   * Maps from a key that is passed to Style.get to the equivalent class
   * specific key.
   */
  private static final Map<String, String> CLASS_SPECIFIC_MAP;

  /**
   * Backing style properties that are used if the style does not
   * defined the property.
   */
  private static final Map<String, GTKStockIcon> ICONS_MAP;

  /**
   * The font used for this particular style, as determined at
   * construction time.
   */
  private final Font font;

  /**
   * Widget type used when looking up class specific values.
   */
  private final int widgetType;

  /**
   * The x/y thickness values for this particular style.
   */
  private final int xThickness, yThickness;

  GTKStyle(Font userFont, WidgetType widgetType) {
    this.widgetType = widgetType.ordinal();

    String pangoFontName;
    synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
      xThickness = nativeGetXThickness(this.widgetType);
      yThickness = nativeGetYThickness(this.widgetType);
      pangoFontName = nativeGetPangoFontName(this.widgetType);
    }

    Font pangoFont = null;
    if (pangoFontName != null) {
      pangoFont = PangoFonts.lookupFont(pangoFontName);
    }
    if (pangoFont != null) {
      this.font = pangoFont;
    } else if (userFont != null) {
      this.font = userFont;
    } else {
      this.font = DEFAULT_FONT;
    }
  }

  @Override
  public void installDefaults(SynthContext context) {
    super.installDefaults(context);
    if (!context.getRegion().isSubregion()) {
      context.getComponent().putClientProperty(
          SwingUtilities2.AA_TEXT_PROPERTY_KEY,
          GTKLookAndFeel.aaTextInfo);
    }
  }

  @Override
  public SynthGraphicsUtils getGraphicsUtils(SynthContext context) {
    return GTK_GRAPHICS;
  }

  /**
   * Returns a <code>SynthPainter</code> that will route the appropriate
   * calls to a <code>GTKEngine</code>.
   *
   * @param state SynthContext identifying requestor
   * @return SynthPainter
   */
  @Override
  public SynthPainter getPainter(SynthContext state) {
    return GTKPainter.INSTANCE;
  }

  protected Color getColorForState(SynthContext context, ColorType type) {
    if (type == ColorType.FOCUS || type == GTKColorType.BLACK) {
      return BLACK_COLOR;
    } else if (type == GTKColorType.WHITE) {
      return WHITE_COLOR;
    }

    Region id = context.getRegion();
    int state = context.getComponentState();
    state = GTKLookAndFeel.synthStateToGTKState(id, state);

    if (type == ColorType.TEXT_FOREGROUND &&
        (id == Region.BUTTON ||
            id == Region.CHECK_BOX ||
            id == Region.CHECK_BOX_MENU_ITEM ||
            id == Region.MENU ||
            id == Region.MENU_ITEM ||
            id == Region.RADIO_BUTTON ||
            id == Region.RADIO_BUTTON_MENU_ITEM ||
            id == Region.TABBED_PANE_TAB ||
            id == Region.TOGGLE_BUTTON ||
            id == Region.TOOL_TIP ||
            id == Region.MENU_ITEM_ACCELERATOR ||
            id == Region.TABBED_PANE_TAB)) {
      type = ColorType.FOREGROUND;
    } else if (id == Region.TABLE ||
        id == Region.LIST ||
        id == Region.TREE ||
        id == Region.TREE_CELL) {
      if (type == ColorType.FOREGROUND) {
        type = ColorType.TEXT_FOREGROUND;
        if (state == SynthConstants.PRESSED) {
          state = SynthConstants.SELECTED;
        }
      } else if (type == ColorType.BACKGROUND) {
        type = ColorType.TEXT_BACKGROUND;
      }
    }

    return getStyleSpecificColor(context, state, type);
  }

  /**
   * Returns color specific to the current style. This method is
   * invoked when other variants don't fit.
   */
  private Color getStyleSpecificColor(SynthContext context, int state,
      ColorType type) {
    state = GTKLookAndFeel.synthStateToGTKStateType(state).ordinal();
    synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
      int rgb = nativeGetColorForState(widgetType, state,
          type.getID());
      return new ColorUIResource(rgb);
    }
  }

  Color getGTKColor(int state, ColorType type) {
    return getGTKColor(null, state, type);
  }

  /**
   * Returns the color for the specified state.
   *
   * @param context SynthContext identifying requestor
   * @param state to get the color for
   * @param type of the color
   * @return Color to render with
   */
  Color getGTKColor(SynthContext context, int state, ColorType type) {
    if (context != null) {
      JComponent c = context.getComponent();
      Region id = context.getRegion();

      state = GTKLookAndFeel.synthStateToGTKState(id, state);
      if (!id.isSubregion() &&
          (state & SynthConstants.ENABLED) != 0) {
        if (type == ColorType.BACKGROUND ||
            type == ColorType.TEXT_BACKGROUND) {
          Color bg = c.getBackground();
          if (!(bg instanceof UIResource)) {
            return bg;
          }
        } else if (type == ColorType.FOREGROUND ||
            type == ColorType.TEXT_FOREGROUND) {
          Color fg = c.getForeground();
          if (!(fg instanceof UIResource)) {
            return fg;
          }
        }
      }
    }

    return getStyleSpecificColor(context, state, type);
  }

  @Override
  public Color getColor(SynthContext context, ColorType type) {
    JComponent c = context.getComponent();
    Region id = context.getRegion();
    int state = context.getComponentState();

    if (c.getName() == "Table.cellRenderer") {
      if (type == ColorType.BACKGROUND) {
        return c.getBackground();
      }
      if (type == ColorType.FOREGROUND) {
        return c.getForeground();
      }
    }

    if (id == Region.LABEL && type == ColorType.TEXT_FOREGROUND) {
      type = ColorType.FOREGROUND;
    }

    // For the enabled state, prefer the widget's colors
    if (!id.isSubregion() && (state & SynthConstants.ENABLED) != 0) {
      if (type == ColorType.BACKGROUND) {
        return c.getBackground();
      } else if (type == ColorType.FOREGROUND) {
        return c.getForeground();
      } else if (type == ColorType.TEXT_FOREGROUND) {
        // If getForeground returns a non-UIResource it means the
        // developer has explicitly set the foreground, use it over
        // that of TEXT_FOREGROUND as that is typically the expected
        // behavior.
        Color color = c.getForeground();
        if (color != null && !(color instanceof UIResource)) {
          return color;
        }
      }
    }
    return getColorForState(context, type);
  }

  protected Font getFontForState(SynthContext context) {
    return font;
  }

  /**
   * Returns the X thickness to use for this GTKStyle.
   *
   * @return x thickness.
   */
  int getXThickness() {
    return xThickness;
  }

  /**
   * Returns the Y thickness to use for this GTKStyle.
   *
   * @return y thickness.
   */
  int getYThickness() {
    return yThickness;
  }

  /**
   * Returns the Insets. If <code>insets</code> is non-null the resulting
   * insets will be placed in it, otherwise a new Insets object will be
   * created and returned.
   *
   * @param context SynthContext identifying requestor
   * @param insets Where to place Insets
   * @return Insets.
   */
  @Override
  public Insets getInsets(SynthContext state, Insets insets) {
    Region id = state.getRegion();
    JComponent component = state.getComponent();
    String name = (id.isSubregion()) ? null : component.getName();

    if (insets == null) {
      insets = new Insets(0, 0, 0, 0);
    } else {
      insets.top = insets.bottom = insets.left = insets.right = 0;
    }

    if (id == Region.ARROW_BUTTON || id == Region.BUTTON ||
        id == Region.TOGGLE_BUTTON) {
      if ("Spinner.previousButton" == name ||
          "Spinner.nextButton" == name) {
        return getSimpleInsets(state, insets, 1);
      } else {
        return getButtonInsets(state, insets);
      }
    } else if (id == Region.CHECK_BOX || id == Region.RADIO_BUTTON) {
      return getRadioInsets(state, insets);
    } else if (id == Region.MENU_BAR) {
      return getMenuBarInsets(state, insets);
    } else if (id == Region.MENU ||
        id == Region.MENU_ITEM ||
        id == Region.CHECK_BOX_MENU_ITEM ||
        id == Region.RADIO_BUTTON_MENU_ITEM) {
      return getMenuItemInsets(state, insets);
    } else if (id == Region.FORMATTED_TEXT_FIELD) {
      return getTextFieldInsets(state, insets);
    } else if (id == Region.INTERNAL_FRAME) {
      insets = Metacity.INSTANCE.getBorderInsets(state, insets);
    } else if (id == Region.LABEL) {
      if ("TableHeader.renderer" == name) {
        return getButtonInsets(state, insets);
      } else if (component instanceof ListCellRenderer) {
        return getTextFieldInsets(state, insets);
      } else if ("Tree.cellRenderer" == name) {
        return getSimpleInsets(state, insets, 1);
      }
    } else if (id == Region.OPTION_PANE) {
      return getSimpleInsets(state, insets, 6);
    } else if (id == Region.POPUP_MENU) {
      return getSimpleInsets(state, insets, 2);
    } else if (id == Region.PROGRESS_BAR || id == Region.SLIDER ||
        id == Region.TABBED_PANE || id == Region.TABBED_PANE_CONTENT ||
        id == Region.TOOL_BAR ||
        id == Region.TOOL_BAR_DRAG_WINDOW ||
        id == Region.TOOL_TIP) {
      return getThicknessInsets(state, insets);
    } else if (id == Region.SCROLL_BAR) {
      return getScrollBarInsets(state, insets);
    } else if (id == Region.SLIDER_TRACK) {
      return getSliderTrackInsets(state, insets);
    } else if (id == Region.TABBED_PANE_TAB) {
      return getTabbedPaneTabInsets(state, insets);
    } else if (id == Region.TEXT_FIELD || id == Region.PASSWORD_FIELD) {
      if (name == "Tree.cellEditor") {
        return getSimpleInsets(state, insets, 1);
      }
      return getTextFieldInsets(state, insets);
    } else if (id == Region.SEPARATOR ||
        id == Region.POPUP_MENU_SEPARATOR ||
        id == Region.TOOL_BAR_SEPARATOR) {
      return getSeparatorInsets(state, insets);
    } else if (id == GTKEngine.CustomRegion.TITLED_BORDER) {
      return getThicknessInsets(state, insets);
    }
    return insets;
  }

  private Insets getButtonInsets(SynthContext context, Insets insets) {
    // The following calculations are derived from gtkbutton.c
    // (GTK+ version 2.8.20), gtk_button_size_allocate() method.
    int CHILD_SPACING = 1;
    int focusSize = getClassSpecificIntValue(context, "focus-line-width", 1);
    int focusPad = getClassSpecificIntValue(context, "focus-padding", 1);
    int xThickness = getXThickness();
    int yThickness = getYThickness();
    int w = focusSize + focusPad + xThickness + CHILD_SPACING;
    int h = focusSize + focusPad + yThickness + CHILD_SPACING;
    insets.left = insets.right = w;
    insets.top = insets.bottom = h;

    Component component = context.getComponent();
    if ((component instanceof JButton) &&
        !(component.getParent() instanceof JToolBar) &&
        ((JButton) component).isDefaultCapable()) {
      // Include the default border insets, but only for JButtons
      // that are default capable.  Note that
      // JButton.getDefaultCapable() returns true by default, but
      // GtkToolButtons are never default capable, so we skip this
      // step if the button is contained in a toolbar.
      Insets defaultInsets = getClassSpecificInsetsValue(context,
          "default-border", BUTTON_DEFAULT_BORDER_INSETS);
      insets.left += defaultInsets.left;
      insets.right += defaultInsets.right;
      insets.top += defaultInsets.top;
      insets.bottom += defaultInsets.bottom;
    }

    return insets;
  }

  /*
   * This is used for both RADIO_BUTTON and CHECK_BOX.
   */
  private Insets getRadioInsets(SynthContext context, Insets insets) {
    // The following calculations are derived from gtkcheckbutton.c
    // (GTK+ version 2.8.20), gtk_check_button_size_allocate() method.
    int focusSize =
        getClassSpecificIntValue(context, "focus-line-width", 1);
    int focusPad =
        getClassSpecificIntValue(context, "focus-padding", 1);
    int totalFocus = focusSize + focusPad;

    // Note: GTKIconFactory.DelegateIcon will have already included the
    // "indicator-spacing" value in the size of the indicator icon,
    // which explains why we use zero as the left inset (or right inset
    // in the RTL case); see 6489585 for more details.
    insets.top = totalFocus;
    insets.bottom = totalFocus;
    if (context.getComponent().getComponentOrientation().isLeftToRight()) {
      insets.left = 0;
      insets.right = totalFocus;
    } else {
      insets.left = totalFocus;
      insets.right = 0;
    }

    return insets;
  }

  private Insets getMenuBarInsets(SynthContext context, Insets insets) {
    // The following calculations are derived from gtkmenubar.c
    // (GTK+ version 2.8.20), gtk_menu_bar_size_allocate() method.
    int internalPadding = getClassSpecificIntValue(context,
        "internal-padding", 1);
    int xThickness = getXThickness();
    int yThickness = getYThickness();
    insets.left = insets.right = xThickness + internalPadding;
    insets.top = insets.bottom = yThickness + internalPadding;
    return insets;
  }

  private Insets getMenuItemInsets(SynthContext context, Insets insets) {
    // The following calculations are derived from gtkmenuitem.c
    // (GTK+ version 2.8.20), gtk_menu_item_size_allocate() method.
    int horizPadding = getClassSpecificIntValue(context,
        "horizontal-padding", 3);
    int xThickness = getXThickness();
    int yThickness = getYThickness();
    insets.left = insets.right = xThickness + horizPadding;
    insets.top = insets.bottom = yThickness;
    return insets;
  }

  private Insets getThicknessInsets(SynthContext context, Insets insets) {
    insets.left = insets.right = getXThickness();
    insets.top = insets.bottom = getYThickness();
    return insets;
  }

  private Insets getSeparatorInsets(SynthContext context, Insets insets) {
    int horizPadding = 0;
    if (context.getRegion() == Region.POPUP_MENU_SEPARATOR) {
      horizPadding =
          getClassSpecificIntValue(context, "horizontal-padding", 3);
    }
    insets.right = insets.left = getXThickness() + horizPadding;
    insets.top = insets.bottom = getYThickness();
    return insets;
  }

  private Insets getSliderTrackInsets(SynthContext context, Insets insets) {
    int focusSize = getClassSpecificIntValue(context, "focus-line-width", 1);
    int focusPad = getClassSpecificIntValue(context, "focus-padding", 1);
    insets.top = insets.bottom =
        insets.left = insets.right = focusSize + focusPad;
    return insets;
  }

  private Insets getSimpleInsets(SynthContext context, Insets insets, int n) {
    insets.top = insets.bottom = insets.right = insets.left = n;
    return insets;
  }

  private Insets getTabbedPaneTabInsets(SynthContext context, Insets insets) {
    int xThickness = getXThickness();
    int yThickness = getYThickness();
    int focusSize = getClassSpecificIntValue(context, "focus-line-width", 1);
    int pad = 2;

    insets.left = insets.right = focusSize + pad + xThickness;
    insets.top = insets.bottom = focusSize + pad + yThickness;
    return insets;
  }

  // NOTE: this is called for ComboBox, and FormattedTextField also
  private Insets getTextFieldInsets(SynthContext context, Insets insets) {
    insets = getClassSpecificInsetsValue(context, "inner-border",
        getSimpleInsets(context, insets, 2));

    int xThickness = getXThickness();
    int yThickness = getYThickness();
    boolean interiorFocus =
        getClassSpecificBoolValue(context, "interior-focus", true);
    int focusSize = 0;

    if (!interiorFocus) {
      focusSize = getClassSpecificIntValue(context, "focus-line-width", 1);
    }

    insets.left += focusSize + xThickness;
    insets.right += focusSize + xThickness;
    insets.top += focusSize + yThickness;
    insets.bottom += focusSize + yThickness;
    return insets;
  }

  private Insets getScrollBarInsets(SynthContext context, Insets insets) {
    int troughBorder =
        getClassSpecificIntValue(context, "trough-border", 1);
    insets.left = insets.right = insets.top = insets.bottom = troughBorder;

    JComponent c = context.getComponent();
    if (c.getParent() instanceof JScrollPane) {
      // This scrollbar is part of a scrollpane; use only the
      // "scrollbar-spacing" style property to determine the padding
      // between the scrollbar and its parent scrollpane.
      int spacing =
          getClassSpecificIntValue(WidgetType.SCROLL_PANE,
              "scrollbar-spacing", 3);
      if (((JScrollBar) c).getOrientation() == JScrollBar.HORIZONTAL) {
        insets.top += spacing;
      } else {
        if (c.getComponentOrientation().isLeftToRight()) {
          insets.left += spacing;
        } else {
          insets.right += spacing;
        }
      }
    } else {
      // This is a standalone scrollbar; leave enough room for the
      // focus line in addition to the trough border.
      if (c.isFocusable()) {
        int focusSize =
            getClassSpecificIntValue(context, "focus-line-width", 1);
        int focusPad =
            getClassSpecificIntValue(context, "focus-padding", 1);
        int totalFocus = focusSize + focusPad;
        insets.left += totalFocus;
        insets.right += totalFocus;
        insets.top += totalFocus;
        insets.bottom += totalFocus;
      }
    }
    return insets;
  }

  /**
   * Returns the value for a class specific property for a particular
   * WidgetType.  This method is useful in those cases where we need to
   * fetch a value for a Region that is not associated with the component
   * currently in use (e.g. we need to figure out the insets for a
   * SCROLL_BAR, but certain values can only be extracted from a
   * SCROLL_PANE region).
   *
   * @param wt WidgetType for which to fetch the value
   * @param key Key identifying class specific value
   * @return Value, or null if one has not been defined
   */
  private static Object getClassSpecificValue(WidgetType wt, String key) {
    synchronized (UNIXToolkit.GTK_LOCK) {
      return nativeGetClassValue(wt.ordinal(), key);
    }
  }

  /**
   * Convenience method to get a class specific integer value for
   * a particular WidgetType.
   *
   * @param wt WidgetType for which to fetch the value
   * @param key Key identifying class specific value
   * @param defaultValue Returned if there is no value for the specified type
   * @return Value, or defaultValue if <code>key</code> is not defined
   */
  private static int getClassSpecificIntValue(WidgetType wt, String key,
      int defaultValue) {
    Object value = getClassSpecificValue(wt, key);
    if (value instanceof Number) {
      return ((Number) value).intValue();
    }
    return defaultValue;
  }

  /**
   * Returns the value for a class specific property. A class specific value
   * is a value that will be picked up based on class hierarchy.
   *
   * @param key Key identifying class specific value
   * @return Value, or null if one has not been defined.
   */
  Object getClassSpecificValue(String key) {
    synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
      return nativeGetClassValue(widgetType, key);
    }
  }

  /**
   * Convenience method to get a class specific integer value.
   *
   * @param context SynthContext identifying requestor
   * @param key Key identifying class specific value
   * @param defaultValue Returned if there is no value for the specified type
   * @return Value, or defaultValue if <code>key</code> is not defined
   */
  int getClassSpecificIntValue(SynthContext context, String key,
      int defaultValue) {
    Object value = getClassSpecificValue(key);

    if (value instanceof Number) {
      return ((Number) value).intValue();
    }
    return defaultValue;
  }

  /**
   * Convenience method to get a class specific Insets value.
   *
   * @param context SynthContext identifying requestor
   * @param key Key identifying class specific value
   * @param defaultValue Returned if there is no value for the specified type
   * @return Value, or defaultValue if <code>key</code> is not defined
   */
  Insets getClassSpecificInsetsValue(SynthContext context, String key,
      Insets defaultValue) {
    Object value = getClassSpecificValue(key);

    if (value instanceof Insets) {
      return (Insets) value;
    }
    return defaultValue;
  }

  /**
   * Convenience method to get a class specific Boolean value.
   *
   * @param context SynthContext identifying requestor
   * @param key Key identifying class specific value
   * @param defaultValue Returned if there is no value for the specified type
   * @return Value, or defaultValue if <code>key</code> is not defined
   */
  boolean getClassSpecificBoolValue(SynthContext context, String key,
      boolean defaultValue) {
    Object value = getClassSpecificValue(key);

    if (value instanceof Boolean) {
      return ((Boolean) value).booleanValue();
    }
    return defaultValue;
  }

  /**
   * Returns the value to initialize the opacity property of the Component
   * to. A Style should NOT assume the opacity will remain this value, the
   * developer may reset it or override it.
   *
   * @param context SynthContext identifying requestor
   * @return opaque Whether or not the JComponent is opaque.
   */
  @Override
  public boolean isOpaque(SynthContext context) {
    Region region = context.getRegion();
    if (region == Region.COMBO_BOX ||
        region == Region.DESKTOP_PANE ||
        region == Region.DESKTOP_ICON ||
        region == Region.EDITOR_PANE ||
        region == Region.FORMATTED_TEXT_FIELD ||
        region == Region.INTERNAL_FRAME ||
        region == Region.LIST ||
        region == Region.MENU_BAR ||
        region == Region.PANEL ||
        region == Region.PASSWORD_FIELD ||
        region == Region.POPUP_MENU ||
        region == Region.PROGRESS_BAR ||
        region == Region.ROOT_PANE ||
        region == Region.SCROLL_PANE ||
        region == Region.SPINNER ||
        region == Region.SPLIT_PANE_DIVIDER ||
        region == Region.TABLE ||
        region == Region.TEXT_AREA ||
        region == Region.TEXT_FIELD ||
        region == Region.TEXT_PANE ||
        region == Region.TOOL_BAR_DRAG_WINDOW ||
        region == Region.TOOL_TIP ||
        region == Region.TREE ||
        region == Region.VIEWPORT) {
      return true;
    }
    Component c = context.getComponent();
    String name = c.getName();
    if (name == "ComboBox.renderer" || name == "ComboBox.listRenderer") {
      return true;
    }
    return false;
  }

  @Override
  public Object get(SynthContext context, Object key) {
    // See if this is a class specific value.
    String classKey = CLASS_SPECIFIC_MAP.get(key);
    if (classKey != null) {
      Object value = getClassSpecificValue(classKey);
      if (value != null) {
        return value;
      }
    }

    // Is it a specific value ?
    if (key == "ScrollPane.viewportBorderInsets") {
      return getThicknessInsets(context, new Insets(0, 0, 0, 0));
    } else if (key == "Slider.tickColor") {
      return getColorForState(context, ColorType.FOREGROUND);
    } else if (key == "ScrollBar.minimumThumbSize") {
      int len =
          getClassSpecificIntValue(context, "min-slider-length", 21);
      JScrollBar sb = (JScrollBar) context.getComponent();
      if (sb.getOrientation() == JScrollBar.HORIZONTAL) {
        return new DimensionUIResource(len, 0);
      } else {
        return new DimensionUIResource(0, len);
      }
    } else if (key == "Separator.thickness") {
      JSeparator sep = (JSeparator) context.getComponent();
      if (sep.getOrientation() == JSeparator.HORIZONTAL) {
        return getYThickness();
      } else {
        return getXThickness();
      }
    } else if (key == "ToolBar.separatorSize") {
      int size = getClassSpecificIntValue(WidgetType.TOOL_BAR,
          "space-size", 12);
      return new DimensionUIResource(size, size);
    } else if (key == "ScrollBar.buttonSize") {
      JScrollBar sb = (JScrollBar) context.getComponent().getParent();
      boolean horiz = (sb.getOrientation() == JScrollBar.HORIZONTAL);
      WidgetType wt = horiz ?
          WidgetType.HSCROLL_BAR : WidgetType.VSCROLL_BAR;
      int sliderWidth = getClassSpecificIntValue(wt, "slider-width", 14);
      int stepperSize = getClassSpecificIntValue(wt, "stepper-size", 14);
      return horiz ?
          new DimensionUIResource(stepperSize, sliderWidth) :
          new DimensionUIResource(sliderWidth, stepperSize);
    } else if (key == "ArrowButton.size") {
      String name = context.getComponent().getName();
      if (name != null && name.startsWith("Spinner")) {
        // Believe it or not, the size of a spinner arrow button is
        // dependent upon the size of the spinner's font.  These
        // calculations come from gtkspinbutton.c (version 2.8.20),
        // spin_button_get_arrow_size() method.
        String pangoFontName;
        synchronized (sun.awt.UNIXToolkit.GTK_LOCK) {
          pangoFontName =
              nativeGetPangoFontName(WidgetType.SPINNER.ordinal());
        }
        int arrowSize = (pangoFontName != null) ?
            PangoFonts.getFontSize(pangoFontName) : 10;
        return (arrowSize + (getXThickness() * 2));
      }
      // For all other kinds of arrow buttons (e.g. combobox arrow
      // buttons), we will simply fall back on the value of
      // ArrowButton.size as defined in the UIDefaults for
      // GTKLookAndFeel when we call UIManager.get() below...
    } else if ("CheckBox.iconTextGap".equals(key) ||
        "RadioButton.iconTextGap".equals(key)) {
      // The iconTextGap value needs to include "indicator-spacing"
      // and it also needs to leave enough space for the focus line,
      // which falls between the indicator icon and the text.
      // See getRadioInsets() and 6489585 for more details.
      int indicatorSpacing =
          getClassSpecificIntValue(context, "indicator-spacing", 2);
      int focusSize =
          getClassSpecificIntValue(context, "focus-line-width", 1);
      int focusPad =
          getClassSpecificIntValue(context, "focus-padding", 1);
      return indicatorSpacing + focusSize + focusPad;
    }

    // Is it a stock icon ?
    GTKStockIcon stockIcon = null;
    synchronized (ICONS_MAP) {
      stockIcon = ICONS_MAP.get(key);
    }

    if (stockIcon != null) {
      return stockIcon;
    }

    // Is it another kind of value ?
    if (key != "engine") {
      // For backward compatibility we'll fallback to the UIManager.
      // We don't go to the UIManager for engine as the engine is GTK
      // specific.
      Object value = UIManager.get(key);
      if (key == "Table.rowHeight") {
        int focusLineWidth = getClassSpecificIntValue(context,
            "focus-line-width", 0);
        if (value == null && focusLineWidth > 0) {
          value = Integer.valueOf(16 + 2 * focusLineWidth);
        }
      }
      return value;
    }

    // Don't call super, we don't want to pick up defaults from
    // SynthStyle.
    return null;
  }

  private Icon getStockIcon(SynthContext context, String key, int type) {
    TextDirection direction = TextDirection.LTR;

    if (context != null) {
      ComponentOrientation co = context.getComponent().
          getComponentOrientation();

      if (co != null && !co.isLeftToRight()) {
        direction = TextDirection.RTL;
      }
    }

    // First try loading a theme-specific icon using the native
    // GTK libraries (native GTK handles the resizing for us).
    Icon icon = getStyleSpecificIcon(key, direction, type);
    if (icon != null) {
      return icon;
    }

    // In a failure case where native GTK (unexpectedly) returns a
    // null icon, we can try loading a default icon as a fallback.
    String propName = ICON_PROPERTY_PREFIX + key + '.' + type + '.' +
        (direction == TextDirection.RTL ? "rtl" : "ltr");
    Image img = (Image)
        Toolkit.getDefaultToolkit().getDesktopProperty(propName);
    if (img != null) {
      return new ImageIcon(img);
    }

    // In an extreme failure situation, just return null (callers are
    // already prepared to handle a null icon, so the worst that can
    // happen is that an icon won't be included in the button/dialog).
    return null;
  }

  private Icon getStyleSpecificIcon(String key,
      TextDirection direction, int type) {
    UNIXToolkit tk = (UNIXToolkit) Toolkit.getDefaultToolkit();
    Image img =
        tk.getStockIcon(widgetType, key, type, direction.ordinal(), null);
    return (img != null) ? new ImageIcon(img) : null;
  }

  static class GTKStockIconInfo {

    private static Map<String, Integer> ICON_TYPE_MAP;
    private static final Object ICON_SIZE_KEY = new StringBuffer("IconSize");

    private static Dimension[] getIconSizesMap() {
      AppContext appContext = AppContext.getAppContext();
      Dimension[] iconSizes = (Dimension[]) appContext.get(ICON_SIZE_KEY);

      if (iconSizes == null) {
        iconSizes = new Dimension[7];
        iconSizes[0] = null;                  // GTK_ICON_SIZE_INVALID
        iconSizes[1] = new Dimension(16, 16); // GTK_ICON_SIZE_MENU
        iconSizes[2] = new Dimension(18, 18); // GTK_ICON_SIZE_SMALL_TOOLBAR
        iconSizes[3] = new Dimension(24, 24); // GTK_ICON_SIZE_LARGE_TOOLBAR
        iconSizes[4] = new Dimension(20, 20); // GTK_ICON_SIZE_BUTTON
        iconSizes[5] = new Dimension(32, 32); // GTK_ICON_SIZE_DND
        iconSizes[6] = new Dimension(48, 48); // GTK_ICON_SIZE_DIALOG
        appContext.put(ICON_SIZE_KEY, iconSizes);
      }
      return iconSizes;
    }

    /**
     * Return the size of a particular icon type (logical size)
     *
     * @param type icon type (GtkIconSize value)
     * @return a Dimension object, or null if lsize is invalid
     */
    public static Dimension getIconSize(int type) {
      Dimension[] iconSizes = getIconSizesMap();
      return type >= 0 && type < iconSizes.length ?
          iconSizes[type] : null;
    }

    /**
     * Change icon size in a type to size mapping. This is called by code
     * that parses the gtk-icon-sizes setting
     *
     * @param type icon type (GtkIconSize value)
     * @param w the new icon width
     * @param h the new icon height
     */
    public static void setIconSize(int type, int w, int h) {
      Dimension[] iconSizes = getIconSizesMap();
      if (type >= 0 && type < iconSizes.length) {
        iconSizes[type] = new Dimension(w, h);
      }
    }

    /**
     * Return icon type (GtkIconSize value) given a symbolic name which can
     * occur in a theme file.
     *
     * @param size symbolic name, e.g. gtk-button
     * @return icon type. Valid types are 1 to 6
     */
    public static int getIconType(String size) {
      if (size == null) {
        return UNDEFINED;
      }
      if (ICON_TYPE_MAP == null) {
        initIconTypeMap();
      }
      Integer n = ICON_TYPE_MAP.get(size);
      return n != null ? n.intValue() : UNDEFINED;
    }

    private static void initIconTypeMap() {
      ICON_TYPE_MAP = new HashMap<String, Integer>();
      ICON_TYPE_MAP.put("gtk-menu", Integer.valueOf(1));
      ICON_TYPE_MAP.put("gtk-small-toolbar", Integer.valueOf(2));
      ICON_TYPE_MAP.put("gtk-large-toolbar", Integer.valueOf(3));
      ICON_TYPE_MAP.put("gtk-button", Integer.valueOf(4));
      ICON_TYPE_MAP.put("gtk-dnd", Integer.valueOf(5));
      ICON_TYPE_MAP.put("gtk-dialog", Integer.valueOf(6));
    }

  }

  /**
   * An Icon that is fetched using getStockIcon.
   */
  private static class GTKStockIcon extends SynthIcon {

    private String key;
    private int size;
    private boolean loadedLTR;
    private boolean loadedRTL;
    private Icon ltrIcon;
    private Icon rtlIcon;
    private SynthStyle style;

    GTKStockIcon(String key, int size) {
      this.key = key;
      this.size = size;
    }

    public void paintIcon(SynthContext context, Graphics g, int x,
        int y, int w, int h) {
      Icon icon = getIcon(context);

      if (icon != null) {
        if (context == null) {
          icon.paintIcon(null, g, x, y);
        } else {
          icon.paintIcon(context.getComponent(), g, x, y);
        }
      }
    }

    public int getIconWidth(SynthContext context) {
      Icon icon = getIcon(context);

      if (icon != null) {
        return icon.getIconWidth();
      }
      return 0;
    }

    public int getIconHeight(SynthContext context) {
      Icon icon = getIcon(context);

      if (icon != null) {
        return icon.getIconHeight();
      }
      return 0;
    }

    private Icon getIcon(SynthContext context) {
      if (context != null) {
        ComponentOrientation co = context.getComponent().
            getComponentOrientation();
        SynthStyle style = context.getStyle();

        if (style != this.style) {
          this.style = style;
          loadedLTR = loadedRTL = false;
        }
        if (co == null || co.isLeftToRight()) {
          if (!loadedLTR) {
            loadedLTR = true;
            ltrIcon = ((GTKStyle) context.getStyle()).
                getStockIcon(context, key, size);
          }
          return ltrIcon;
        } else if (!loadedRTL) {
          loadedRTL = true;
          rtlIcon = ((GTKStyle) context.getStyle()).
              getStockIcon(context, key, size);
        }
        return rtlIcon;
      }
      return ltrIcon;
    }
  }

  /**
   * GTKLazyValue is a slimmed down version of <code>ProxyLaxyValue</code>.
   * The code is duplicate so that it can get at the package private
   * classes in gtk.
   */
  static class GTKLazyValue implements UIDefaults.LazyValue {

    /**
     * Name of the class to create.
     */
    private String className;
    private String methodName;

    GTKLazyValue(String name) {
      this(name, null);
    }

    GTKLazyValue(String name, String methodName) {
      this.className = name;
      this.methodName = methodName;
    }

    public Object createValue(UIDefaults table) {
      try {
        Class c = Class.forName(className, true, Thread.currentThread().
            getContextClassLoader());

        if (methodName == null) {
          return c.newInstance();
        }
        Method m = c.getMethod(methodName, (Class[]) null);

        return m.invoke(c, (Object[]) null);
      } catch (ClassNotFoundException cnfe) {
      } catch (IllegalAccessException iae) {
      } catch (InvocationTargetException ite) {
      } catch (NoSuchMethodException nsme) {
      } catch (InstantiationException ie) {
      }
      return null;
    }
  }

  static {
    CLASS_SPECIFIC_MAP = new HashMap<String, String>();
    CLASS_SPECIFIC_MAP.put("Slider.thumbHeight", "slider-width");
    CLASS_SPECIFIC_MAP.put("Slider.trackBorder", "trough-border");
    CLASS_SPECIFIC_MAP.put("SplitPane.size", "handle-size");
    CLASS_SPECIFIC_MAP.put("Tree.expanderSize", "expander-size");
    CLASS_SPECIFIC_MAP.put("ScrollBar.thumbHeight", "slider-width");
    CLASS_SPECIFIC_MAP.put("ScrollBar.width", "slider-width");
    CLASS_SPECIFIC_MAP.put("TextArea.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("TextArea.caretAspectRatio", "cursor-aspect-ratio");
    CLASS_SPECIFIC_MAP.put("TextField.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("TextField.caretAspectRatio", "cursor-aspect-ratio");
    CLASS_SPECIFIC_MAP.put("PasswordField.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("PasswordField.caretAspectRatio", "cursor-aspect-ratio");
    CLASS_SPECIFIC_MAP.put("FormattedTextField.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("FormattedTextField.caretAspectRatio", "cursor-aspect-");
    CLASS_SPECIFIC_MAP.put("TextPane.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("TextPane.caretAspectRatio", "cursor-aspect-ratio");
    CLASS_SPECIFIC_MAP.put("EditorPane.caretForeground", "cursor-color");
    CLASS_SPECIFIC_MAP.put("EditorPane.caretAspectRatio", "cursor-aspect-ratio");

    ICONS_MAP = new HashMap<String, GTKStockIcon>();
    ICONS_MAP.put("FileChooser.cancelIcon", new GTKStockIcon("gtk-cancel", 4));
    ICONS_MAP.put("FileChooser.okIcon", new GTKStockIcon("gtk-ok", 4));
    ICONS_MAP.put("OptionPane.errorIcon", new GTKStockIcon("gtk-dialog-error", 6));
    ICONS_MAP.put("OptionPane.informationIcon", new GTKStockIcon("gtk-dialog-info", 6));
    ICONS_MAP.put("OptionPane.warningIcon", new GTKStockIcon("gtk-dialog-warning", 6));
    ICONS_MAP.put("OptionPane.questionIcon", new GTKStockIcon("gtk-dialog-question", 6));
    ICONS_MAP.put("OptionPane.yesIcon", new GTKStockIcon("gtk-yes", 4));
    ICONS_MAP.put("OptionPane.noIcon", new GTKStockIcon("gtk-no", 4));
    ICONS_MAP.put("OptionPane.cancelIcon", new GTKStockIcon("gtk-cancel", 4));
    ICONS_MAP.put("OptionPane.okIcon", new GTKStockIcon("gtk-ok", 4));
  }
}
